﻿#region License
// Copyright 2010 Buu Nguyen (http://www.buunguyen.net/blog)
// 
// Licensed under the Apache License, Version 2.0 (the "License"); 
// you may not use this file except in compliance with the License. 
// You may obtain a copy of the License at 
// 
// http://www.apache.org/licenses/LICENSE-2.0 
// 
// Unless required by applicable law or agreed to in writing, software 
// distributed under the License is distributed on an "AS IS" BASIS, 
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 
// See the License for the specific language governing permissions and 
// limitations under the License.
// 
// The latest version of this file can be found at http://www.codeplex.com/aspmvcvalidation
#endregion

using System.Collections.Generic;
using System.Web.Mvc;
using System.Linq;
using Microsoft.Practices.EnterpriseLibrary.Validation;
using Vab = Microsoft.Practices.EnterpriseLibrary.Validation;

namespace Mvc.Validation
{
    /// <summary>
    /// This class contains extension methods to support both 
    /// server-side validation and client-side script generation.
    /// </summary>
    public static class ValidationUtils
    {
        /// <summary>
        /// Regular expression pattern for URLs
        /// </summary>
        public const string UrlPattern = @"(^$)|(^(https?|ftp):\/\/(((([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(%[\da-f]{2})|[!\$&'\(\)\*\+,;=]|:)*@)?(((\d|[1-9]\d|1\d\d|2[0-4]\d|25[0-5])\.(\d|[1-9]\d|1\d\d|2[0-4]\d|25[0-5])\.(\d|[1-9]\d|1\d\d|2[0-4]\d|25[0-5])\.(\d|[1-9]\d|1\d\d|2[0-4]\d|25[0-5]))|((([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])*([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])))\.)+(([a-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(([a-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])*([a-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])))\.?)(:\d*)?)(\/((([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(%[\da-f]{2})|[!\$&'\(\)\*\+,;=]|:|@)+(\/(([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(%[\da-f]{2})|[!\$&'\(\)\*\+,;=]|:|@)*)*)?)?(\?((([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(%[\da-f]{2})|[!\$&'\(\)\*\+,;=]|:|@)|[\uE000-\uF8FF]|\/|\?)*)?(\#((([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(%[\da-f]{2})|[!\$&'\(\)\*\+,;=]|:|@)|\/|\?)*)?$)";

        /// <summary>
        /// Regular expression pattern for emails
        /// </summary>
        public const string EmailPattern = @"(^$)|(^((([a-z]|\d|[!#\$%&'\*\+\-\/=\?\^_`{\|}~]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])+(\.([a-z]|\d|[!#\$%&'\*\+\-\/=\?\^_`{\|}~]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])+)*)|((\x22)((((\x20|\x09)*(\x0d\x0a))?(\x20|\x09)+)?(([\x01-\x08\x0b\x0c\x0e-\x1f\x7f]|\x21|[\x23-\x5b]|[\x5d-\x7e]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(\\([\x01-\x09\x0b\x0c\x0d-\x7f]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF]))))*(((\x20|\x09)*(\x0d\x0a))?(\x20|\x09)+)?(\x22)))@((([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])*([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])))\.)+(([a-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(([a-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])*([a-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])))\.?$)";

        /// <summary>
        /// Regular expression pattern for dates
        /// </summary>
        public const string DatePattern = @"(^$)|(^\d\d?\/\d\d?\/\d\d\d\d$)";

        /// <summary>
        /// Regular expression pattern for alphanumeric values
        /// </summary>
        public const string AlphaNumericPattern = @"^\w+$";

        /// <summary>
        /// Validates all annotated properties of the specified <paramref name="entity"/>.
        /// </summary>
        /// <param name="entity">The entity to be validated.</param>
        /// <exception cref="EntityValidationException">when there is one or more validation errors.</exception>
        public static void Validate<T>(this T entity)
        {
            entity.Validate(null);
        }

        /// <summary>
        /// Validates the properties specified in the <paramref name="includedProperties"/>
        /// parameter.  If <paramref name="includedProperties"/> is null, all annotated 
        /// properties will be validated.
        /// </summary>
        /// <param name="entity">The entity to be validated.</param>
        /// <param name="includedProperties">Array of properties to be included in the validation.</param>
        /// <exception cref="EntityValidationException">when there is one or more validation errors.</exception>
        public static void Validate<T>(this T entity, params string[] includedProperties)
        {
            ValidationResults results = Vab.Validation.Validate(entity);

            results = FilterResults(results, includedProperties);

            if (!results.IsValid)
            {
                throw new EntityValidationException(results);
            }
        }

        /// <summary>
        /// Selects only results for properties included in the <paramref name="includedProperties"/>.
        /// </summary>
        private static ValidationResults FilterResults(ValidationResults results, 
            IEnumerable<string> includedProperties)
        {
            if (includedProperties == null)
                return results;

            var filteredList = new ValidationResults();
            filteredList.AddAllResults(from result in results
                                       where includedProperties.Any(
                                           prop => prop.ToLower() == result.Key.ToLower())
                                       select result);

            return filteredList;
        }

        /// <summary>
        /// Populates the <paramref name="modelState"/> with all errors in the 
        /// exception object <paramref name="ex"/>.
        /// </summary>
        /// <param name="modelState">The view model state which needs to be populated with 
        /// error information.</param>
        /// <param name="ex">The exception from which errors will be retrieved.</param>
        public static void PopulateWithErrors(
             this ModelStateDictionary modelState, EntityValidationException ex)
        {
            modelState.PopulateWithErrors(ex, null);
        }

        /// <summary>
        /// Populates the <paramref name="modelState"/> with all errors in the 
        /// exception object <paramref name="ex"/>.
        /// </summary>
        /// <param name="modelState">The view model state which needs to be populated with 
        /// error information.</param>
        /// <param name="ex">The exception from which errors will be retrieved.</param>
        /// <param name="prefix">The prefix of property name.</param>
        public static void PopulateWithErrors(this ModelStateDictionary modelState, 
            EntityValidationException ex, string prefix)
        {
            prefix = prefix == null ? string.Empty : prefix + ".";
            foreach (var error in ex.Errors)
            {
                modelState.AddModelError(prefix + error.Key, error.Message);
            }
        }

        /// <summary>
        /// Invokes <code>ClientSideValidation(formName, ignoreMissingElements)</code> with
        /// <code>formName</code> as null and <code>ignoreMissingElements</code> as false.
        /// </summary>
        public static string ClientSideValidation<T>(this HtmlHelper html)
        {
            return html.ClientSideValidation<T>(false);
        }

        /// <summary>
        /// Invokes <code>ClientSideValidation(formName, ignoreMissingElements)</code> with
        /// <code>formName</code> as null.
        /// </summary>
        public static string ClientSideValidation<T>(this HtmlHelper html,
            bool ignoreMissingElement)
        {
            return html.ClientSideValidation<T>(null, ignoreMissingElement);
        }

        /// <summary>
        /// Invokes <code>ClientSideValidation(formName, ignoreMissingElements)</code> with
        /// <code>ignoreMissingElements</code> as false.
        /// </summary>
        public static string ClientSideValidation<T>(this HtmlHelper html, string formName)
        {
            return html.ClientSideValidation<T>(formName, false);
        }

        /// <summary>
        /// Invokes <code>ClientSideValidation(formName, ignoreMissingElements, readyFunctionName)</code> with
        /// <code>readyFunctionName</code> as <code>null</code> (i.e. use default jQuery's ready 
        /// function).
        /// </summary>
        public static string ClientSideValidation<T>(this HtmlHelper html, string formName,
            bool ignoreMissingElements)
        {
            return html.ClientSideValidation<T>(formName, ignoreMissingElements, null);
        }

        /// <summary>
        /// Invokes <code>ClientSideValidation(formName, ignoreMissingElements, readyFunctionName, validateFunctionName)</code> 
        /// with <code>validateFunctionName</code> as <code>null</code> (i.e. use default jQuery Validate function).
        /// </summary>
        public static string ClientSideValidation<T>(this HtmlHelper html, string formName,
            bool ignoreMissingElements, string readyFunctionName)
        {
            return html.ClientSideValidation<T>(formName, ignoreMissingElements, readyFunctionName, null);
        }

        /// <summary>
        /// Generates client-side validation code.
        /// 
        /// If <paramref name="formName"/> is null or empty, the <code>$("#formId").validate(...)</code> 
        /// won't be emitted and you are supposed to write that code somewhere in your final 
        /// page output for the client-side validation to work. 
        /// 
        /// If <paramref name="formName"/> is not null or empty and if 
        /// <param name="validateFunctionName" /> is specified, the function is used 
        /// to register the validation.  For example if <param name="validateFunctionName" /> is
        /// <code>setupFormValidation</code> then instead of emitting <code>$("#formId").validate(...)</code>,
        /// the library will emit <code>setupFormValidation("#formId")</code>.  
        /// 
        /// If <paramref name="ignoreMissingElements"/> is true, the undefined-checking code will
        /// be emitted for each element.
        /// 
        /// Sample format of generated script (with <param name="ignoreMissingElements"/> is false)
        /// is as follows:
        /// 
        /// $().ready(function() {
        ///     $("#id").rules("add", {
        ///         required: val,
        ///         minlength: len,
        ///         messages: {
        ///             required: msg,
        ///             minlength: msg
        ///         }
        ///     });
        /// 
        ///     ... repeat for other properties ...
        /// });
        /// 
        /// If <param name="readyFunctionName" /> is specified, it is used in place of jQuery's initialization
        /// method.  For example if <param name="readyFunctionName" /> is <code>onLoad</code>, then instead 
        /// of emitting <code>$().ready(function() {...})</code>, the library will emit 
        /// <code>onLoad(function(){...})</code>.
        /// </summary>
        public static string ClientSideValidation<T>(this HtmlHelper html, string formName,
            bool ignoreMissingElements, string readyFunctionName, string validateFunctionName)
        {
            return new JQueryCodeGenerator().GenerateCode<T>(formName, ignoreMissingElements, 
                validateFunctionName, readyFunctionName);
        }
    }
}
